'use strict'

const _ = require('lodash');
const DataFrame = require('./dataFrame.js').DataFrame;
const proParams = require('./params.js');
const utils = require('../helpers/utils.js');

class SyncClockCmd extends DataFrame {
  /**
   * @description Constructor of sync clock command frame
   * @param {string} mac - mac address 8 bytes with hex string format
   * @param {object} options - init values
   * @param {int} options.year - year
   * @param {int} options.month - 1 - 12
   * @param {int} options.day - 1 - 31
   * @param {int} options.week - 1 - 7
   * @param {int} options.hour - 0 - 23
   * @param {int} options.minute - 0 - 59
   * @param {int} options.second - 0 - 59
   * @returns {object}
   */
  constructor(mac, options) {
    let buf = new Buffer.allocUnsafe(8);
    buf.writeUInt16BE(parseInt(options.year,10), 0);
    buf[2] = parseInt(options.month, 10);
    buf[3] = parseInt(options.day, 10);
    buf[4] = parseInt(options.week, 10);
    buf[5] = parseInt(options.hour, 10);
    buf[6] = parseInt(options.minute, 10);
    buf[7] = parseInt(options.second, 10);
    super({
      mac: mac,
      data: buf,
      cmd: proParams.CMDS.SYNC_CLOCK,
      res: 0xff
    });
  }
}

class WorkModeCmd extends DataFrame {
  /**
   * @description Constructor of sync clock command frame
   * @param {string} mac - mac address 8 bytes with hex string format
   * @param {int} mode - 0 or 1
   * @returns {object}
   */  
  constructor(mac, mode) {
    let buf = new Buffer.allocUnsafe(1);
    buf[0] = mode;
    super({
      mac: mac,
      data: buf,
      cmd: proParams.CMDS.WORK_MODE,
      res: 0xff
    });
  }
}

class ControllerQueryCmd extends DataFrame {
  /**
   * @description Constructor of query device status command frame
   * @param {string} mac - mac address 8 bytes with hex string format
   * @param {Array} fields - query fields key
   * @returns {object}
   */  
  constructor(mac, fields) {
    let query_fields = _.filter(fields, (k)=> {
      return _.has(proParams.DEVICE_VALUES.CONTROLLER_STATUS_FIELDS, k);
    });    
    let buf = new Buffer.allocUnsafe(2*query_fields.length);
    query_fields.forEach((key, index)=> {
      buf.writeUInt16BE(proParams.DEVICE_VALUES.CONTROLLER_STATUS_FIELDS[key], 2*(index));
    });
    super({
      mac: mac,
      data: buf,
      cmd: proParams.CMDS.CTRL_STATUS,
      res: 0xff
    });
  }
}

class DeviceDimmingCmd extends DataFrame {
  /**
   * @description Constructor of device dimming command frame
   * @param {string} mac - mac address 8 bytes with hex string format
   * @param {Array} targets - array of dimming parameters
   * @param {object} targets.item - one params
   * @param {string} targets.item.device - device mac address
   * @param {int} targets.item.ch1 - R channel
   * @param {int} targets.item.ch2 - G channel
   * @param {int} targets.item.ch3 - W channel
   * @param {int} targets.item.ch4 - FR channel
   * @returns {object}
   */
  constructor(mac, targets) {
    let buf = new Buffer.allocUnsafe(12*targets.length);
    _.forEach(targets, (params, index)=> {
      let device = utils.pad(proParams.DEVICE_VALUES.MAC_PADDING, params.device, true);
      let pos = 12*index;
      buf.writeUInt32BE(parseInt(device.substring(0, 8), 16), pos);
      buf.writeUInt32BE(parseInt(device.substring(8, 16), 16), pos+4);
      buf.writeUInt8(parseInt(params.ch1, 10), pos+8);
      buf.writeUInt8(parseInt(params.ch2, 10), pos+9);
      buf.writeUInt8(parseInt(params.ch3, 10), pos+10);
      buf.writeUInt8(parseInt(params.ch4, 10), pos+11);
    })
    super({
      mac: mac,
      data: buf,
      cmd: proParams.CMDS.DIMMNG,
      res: 0xff
    });
  }
}

class DeviceSwitchCmd extends DataFrame {
  /**
   * @description Constructor of device switch command frame
   * @param {string} mac - mac address 8 bytes with hex string format
   * @param {Array} targets - array of dimming parameters
   * @param {object} targets.item - one params
   * @param {string} targets.item.device - device mac address
   * @param {int} targets.item.ch1 - 0: off, 1: on, 3: invalid
   * @param {int} targets.item.ch2 - 0: off, 1: on, 3: invalid
   * @param {int} targets.item.ch3 - 0: off, 1: on, 3: invalid
   * @param {int} targets.item.ch4 - 0: off, 1: on, 3: invalid
   * @returns {object}
   */
  constructor(mac, targets) {
    let buf = new Buffer.allocUnsafe(9*targets.length);
    let key = 'SWITCH';
    _.forEach(targets, (params, index)=> {
      let device = utils.pad(proParams.DEVICE_VALUES.MAC_PADDING, params.device, true);
      let pos = 9*index;
      buf.writeUInt32BE(parseInt(device.substring(0, 8), 16), pos);
      buf.writeUInt32BE(parseInt(device.substring(8, 16), 16), pos+4);
      let v = utils.Enum2Val(proParams.EnumValues, key, params.ch1, 'invalid');
      v +=  (utils.Enum2Val(proParams.EnumValues, key, params.ch2, 'invalid') << 2);
      v +=  (utils.Enum2Val(proParams.EnumValues, key, params.ch3, 'invalid') << 4);
      v +=  (utils.Enum2Val(proParams.EnumValues, key, params.ch4, 'invalid') << 6);
      buf.writeUInt8(v, pos+8);
    })
    super({
      mac: mac,
      data: buf,
      cmd: proParams.CMDS.SWITCH,
      res: 0xff
    });
  }
}

class DeviceQueryCmd extends DataFrame {
  /**
   * @description Constructor of query lamp device command frame
   * @param {string} mac - mac address 8 bytes with hex string format
   * @param {Array} targets - query device list with fields
   * @param {string} targets.item.device - device mac
   * @param {Array} targets.item.fields - query fields key
   * @returns {object}
   */  
  constructor(mac, targets) {
    // filter the query fields and calculate total size
    let total_size = 0;
    targets.forEach((item)=> {
      item.device =  utils.pad(proParams.DEVICE_VALUES.MAC_PADDING, item.device, true);
      item.fields = _.filter(item.fields, (k)=> {
        return _.has(proParams.DEVICE_VALUES.DEVICE_STATUS_FIELDS, k);
      });
      total_size += 2*item.fields.length+8+2;    
    });

    let pos = 0;
    let buf = new Buffer.allocUnsafe(total_size);
    targets.forEach((item)=> {
      let len = 2*item.fields.length + 8;
      // devide status length
      buf.writeUInt16BE(len, pos);
      pos += 2;
      // device mac
      buf.writeUInt32BE(parseInt(item.device.substring(0, 8), 16), pos);
      buf.writeUInt32BE(parseInt(item.device.substring(8, 16), 16), pos+4);
      pos += 8;    
      item.fields.forEach((key, index)=> {
        buf.writeUInt16BE(proParams.DEVICE_VALUES.DEVICE_STATUS_FIELDS[key], pos);
        pos += 2;
      });
    });
    super({
      mac: mac,
      data: buf,
      cmd: proParams.CMDS.DEV_STATUS,
      res: 0xff
    });
  }
}

const _ver_template = utils.template`${0}.${1}.${2}.${3}`;

function _decoding(buf, start, end) {
    let encoding = undefined;
    switch(buf[start]) {
    case 0x00:  // ascii
      encoding = 'ascii';
      break;
    case 0x01:
      encoding = 'utf8';
      break;
    case 0x02:
      encoding = 'utf16';
      break;
    default:
      break;
    }
    return buf.toString(encoding, start+1, end);
}

/**
 * @description retrieve data of controller status response frame
 * @param {Buffer} buf - response data
 * @returns {Object}
 */ 
function _retrieve_controller_status(buf) {
  let data = {};
  let pos = 0; 
  let total_size = buf.readUInt16BE(pos);
  pos += 2;
  if ((buf.length-2) !== total_size) {
    // invalid response frame
    return data;
  }

  while (pos < buf.length) {
    let id = buf.readUInt16BE(pos);
    let support = buf.readUInt8(pos+2);
    let len = buf.readUInt16BE(pos+3);
    pos += 5;
    if (support === 0) {
      switch (id) {
      case 0x0001:
        if (len === 4) {
          data.v_software = _ver_template(buf[pos], buf[pos+1], buf[pos+2], buf[pos+3]);
        }        
        break;
      case 0x0002:        
        if (len === 4) {
          data.v_system = _ver_template(buf[pos], buf[pos+1], buf[pos+2], buf[pos+3]);
        }
        break;
      case 0x0003:
        if (len === 4) {
          data.v_kernel = _ver_template(buf[pos], buf[pos+1], buf[pos+2], buf[pos+3]);
        }
        break;
      case 0x0004:
        if (len === 4) {
          data.v_hardware = _ver_template(buf[pos], buf[pos+1], buf[pos+2], buf[pos+3]);
        }
        break;
      case 0x0005:
        if (len >= 1) {
          data.manufacturer = _decoding(buf, pos, pos+len);
        }
        break;
      case 0x0006:
        if (len === 1) {
          data.type = buf[pos];
        }
        break;
      case 0x0007:
        if (len === 1) {
          data.work_mode = buf[pos];
        }
        break;
      case 0x0008:
        if (len === 1) {
          data.status = buf[pos];
        }
        break;
      default:
        break;
      }
    }
    pos += len;
  }

  return data;
}

/**
 * @description retrieve data of device dimming response frame
 * @param {Buffer} buf - response data
 * @returns {Array}
 */ 
function _retrieve_device_dimming(buf) {
  let data = [];
  let pos = 0; 
  if ((buf.length % 9) !== 0) {
    // invalid response frame
    return data;
  }

  while (pos < buf.length) {
    data.push({
      device: utils.pad(proParams.DEVICE_VALUES.MAC_PADDING, buf.toString('hex', pos, pos+8).toUpperCase(), true),
      response: utils.Bin2Enum(proParams.StatusEnums, 'DIMMING_STATUS', buf[pos+8])
    });
    pos += 9;
  }
  return data;
}

/**
 * @description retrieve data of device switch response frame
 * @param {Buffer} buf - response data
 * @returns {Array}
 */ 
function _retrieve_device_switch(buf) {
  let data = [];
  let pos = 0; 
  if ((buf.length % 9) !== 0) {
    // invalid response frame
    return data;
  }

  while (pos < buf.length) {
    data.push({
      device: utils.pad(proParams.DEVICE_VALUES.MAC_PADDING, buf.toString('hex', pos, pos+8).toUpperCase(), true),
      response: utils.Bin2Enum(proParams.StatusEnums, 'SWITCH_STATUS', (buf[pos+8]&0x03))
    });
    pos += 9;
  }
  return data;
}

function _brightness_to_string(val) {
  let rets = [
    (val & 0xFF000000) >> 24,
    (val & 0x00FF0000) >> 16,
    (val & 0x0000FF00) >> 8,
    (val & 0x000000FF)
  ];
  return rets.join(',');  
}

/**
 * @description retrieve data of device status response frame
 * @param {Buffer} buf - response data
 * @returns {Array}
 */ 
function _retrieve_device_status(buf) {
  let data = [];
  let pos = 0;

  while (pos < buf.length) {
    // current record length
    let len = buf.readUInt16BE(pos);
    pos += 2;
    if (len > 8) {
      let record = {
        device: utils.pad(proParams.DEVICE_VALUES.MAC_PADDING, buf.toString('hex', pos, pos+8).toUpperCase(), true)
      }
      let rec_pos = pos+8;
      while (rec_pos < pos+len ) {
        let id = buf.readUInt16BE(rec_pos);
        let support = buf.readUInt8(rec_pos+2);
        let rec_len = buf.readUInt16BE(rec_pos+3);
        rec_pos += 5;
        switch(id) {
        case 0x0001:  // software version 1 byte
          if (rec_len === 1) {
            record.v_software = buf[rec_pos];
          }
          break;
        case 0x0002:  // manufactorer 1 byte(encoding) + data
          if (rec_len >= 1) {
            record.manufacturer = _decoding(buf, rec_pos, rec_pos+rec_len);
          }
          break;
        case 0x0003:  // device type 1 byte
          if (rec_len === 1) {
            record.type = buf[rec_pos];
          }
          break;
        case 0x1001:  // input volt 2 bytes
          if (rec_len === 2) {
            let v = buf.readUInt16BE(rec_pos);
            let level = Math.pow(10, (v&0xC000)>> 14);
            record.volt = level * (v&(0x3FFF)) / 1000;
          }
          break;
        case 0x1002:  // input amp 2 bytes
          if (rec_len === 2) {
            let v = buf.readUInt16BE(rec_pos);
            let level = Math.pow(10, (v&0xC000) >> 14);
            record.amp = level * (v&(0x3FFF)) / 1000;
          }
          break;
        case 0x1003:  // power 2 bytes
          if (rec_len === 2) {
            let v = buf.readUInt16BE(rec_pos);
            let level = Math.pow(10, (v&0xC000) >> 14);
            record.power = level * (v&(0x3FFF)) / 1000;
          }
          break;
        case 0x1004:  // e_consumption 4 bytes
          if (rec_len === 4) {
            let v = buf.readUInt32BE(rec_pos);
            record.e_consumption = v / 100;
          }
          break;
        case 0x2001:  // minimum brightness 4 bytes
          if (rec_len === 4) {            
            record.brightness_min = buf.toString('hex', rec_pos, rec_pos+4);
          }
          break;
        case 0x2002:  // maximum brightness 4 bytes
          if (rec_len === 4) {
            record.brightness_max = buf.toString('hex', rec_pos, rec_pos+4);
          }
          break;
        case 0x2003:  // power on brightness 4 bytes
          if (rec_len === 4) {
            record.brightness_power_on = buf.toString('hex', rec_pos, rec_pos+4);
          }
          break;
        case 0x2004:  // fault brightness 4 bytes
          if (rec_len === 4) {
            record.brightness_fault = buf.toString('hex', rec_pos, rec_pos+4);
          }
          break;
        case 0x2005:  // current brightness 4 bytes
          if (rec_len === 4) {
            record.brightness = buf.toString('hex', rec_pos, rec_pos+4);
          }
          break;
        case 0x2006:  // switch status 1 byte
          if (rec_len === 1) {
            record.switch = buf[rec_pos];
          }
          break;
        case 0x2007:  // transition duration 1 byte
          if (rec_len === 1) {
            record.transition_duration = buf[rec_pos];
          }
          break;
        case 0x2008:  // brightness coefficiency 1 byte
          if (rec_len === 1) {
            record.coefficiency = buf[rec_pos];
          }
          break;
        case 0x3001:  // total work time 4 bytes with minutes 
          if (rec_len === 4) {
            record.total_uptime = buf.readUInt32BE(rec_pos);
          }
          break;
        case 0x3002:  // device status 1 byte
          if (rec_len === 1) {
            record.status = buf[rec_pos];
          }
          break;
        case 0x3003:  // device comm status 1 byte
          if (rec_len === 1) {
            record.comm_status = buf[rec_pos];
          }
          break;
        case 0x3004:  // device lamp status 1 byte
          if (rec_len === 1) {
            record.lamp_status = buf[rec_pos];
          }
          break;
        case 0x3005:  // device total lighting time 4 bytes with minutes
          if (rec_len === 4) {
            record.lighting_time =  buf.readUInt32BE(rec_pos);
          }
          break;
        }
        rec_pos += rec_len;
      }
      data.push(record);
    }
    // goto next device record
    pos += len;    
  }
  return data;
}

/**
 * @description retrieve file download status response frame
 * @param {Buffer} buf - response data
 * @returns {Array}
 */ 
function _retrieve_file_download(buf) {
  if (buf.length !== 4) {
    // invalid response frame
    return null;
  }

  return {
    file_type: buf[0],
    sn: (buf[1] << 16) + buf.readUInt16BE(2)
  }
}

const response_handler = {
  // 0x0001: function(data) { return null},
  // 0x0002: function(data) { return null},
  // 0x0003: function(data) { return null},
  // 0x0004: function(data) { return null},
  0x1001: _retrieve_controller_status,
  0x2001: _retrieve_device_dimming,
  0x2002: _retrieve_device_switch,
  0x3001: _retrieve_device_status,
  0x4001: _retrieve_file_download,
  0x4002: function(data) { return null}
}

/**
 * @description retrieve data of response frame
 * @param {DataFrame} dataFrame - frame object
 * @returns {Object}
 */ 
function retrieveResponseFrame(dataFrame) {
  let results = {
    res: utils.Bin2Enum(proParams.StatusEnums, 'CMD_RES_STATUS', dataFrame.res()),
    data: null
  };

  if (results.res !== "Success") {
    return results;
  }

  if (_.has(response_handler, dataFrame.cmd()) === false) {
    return results;
  }

  let handler = response_handler[dataFrame.cmd()];
  results.data = handler(dataFrame.data());
  return results;
}


module.exports.CMD = {
  syncClock: SyncClockCmd,
  workMode: WorkModeCmd,
  queryController: ControllerQueryCmd,
  dimming: DeviceDimmingCmd,
  onoff: DeviceSwitchCmd,
  queryDevice: DeviceQueryCmd
}

module.exports.retrieveResponse = retrieveResponseFrame;